#include <iostream>
#include <cstring>
#include <fstream>
#include <vector>
#include <cmath>
#include <ctime>
#include <limits>
#include <iomanip>
#include <stdexcept>
#include <algorithm>

#include "XMLParser.h"
#include "Arguments.h"

// ----------------------------------------------------------------------------

namespace gpxtools
{
  class GPXGeoTag : public XMLParserHandler
  {
  public:
    // -- Constructor -----------------------------------------------------------
    GPXGeoTag() :
      _arguments("gpxgeotag [OPTION].. [GPX-FILE] [JPG-FILE]\nGeo tag the photos with the GPX-files.\n", "gpxgeotag v0.1",
                 "Geo tag the photos with the GPX-files, based on the time of the photos and the geopositions in the GPX-files."),
      _seconds  (_arguments, true,  's', "seconds",  "SEC",                 "the seconds offset for the time of the photos", ""),
      _minutes  (_arguments, true,  'm', "minutes",  "MIN",                 "the minutes offset for the time of the photos", ""),
      _hours    (_arguments, true,  'h', "hours",    "HOURS",               "the hours offset for the time of the photos", ""),
      _xmlParser(this),
      _offset(0),
      _jpegnames(),
      _gpxnames(),
      _segments(),
      _timeStr()
    {
    }

    // -- Deconstructor ---------------------------------------------------------
    virtual ~GPXGeoTag()
    {
    }

    // -- Parse arguments -------------------------------------------------------
    bool processArguments(int argc, char *argv[])
    {
      std::vector<std::string> filenames;

      if (!_arguments.parse(argc,argv, filenames))
      {
        return false;
      }
      else if (!checkArguments(filenames))
      {
        return false;
      }
      else if (_gpxnames.empty())
      {
        return _xmlParser.parse(std::cin);
      }
      else
      {
        for (auto filename = _gpxnames.begin(); filename != _gpxnames.end(); ++filename)
        {
          if (!parseFile(*filename)) return false;
        }
      }

      return true;
    }

    // -- Check arguments ---------------------------------------------------------
    bool checkArguments(const std::vector<std::string> &filenames)
    {
      _offset = 0;

      if (!_seconds.value().empty())
      {
        try
        {
          _offset += std::stoi(_seconds.value());
        }
        catch (...)
        {
          std::cerr << "Invalid value for --" << _seconds.longOption() << " : " << _seconds.value() << std::endl;
          return false;
        }
      }

      if (!_minutes.value().empty())
      {
        try
        {
          _offset += (std::stoi(_minutes.value()) * 60);
        }
        catch (...)
        {
          std::cerr << "Invalid value for --" << _minutes.longOption() << " : " << _minutes.value() << std::endl;
          return false;
        }
      }

      if (!_hours.value().empty())
      {
        try
        {
          _offset += (std::stoi(_hours.value()) * 3600);
        }
        catch (...)
        {
          std::cerr << "Invalid value for --" << _hours.longOption() << " : " << _hours.value() << std::endl;
          return false;
        }
      }

      for (auto filename = filenames.begin(); filename != filenames.end(); ++filename)
      {
        std::size_t pos = filename->find_last_of('.');

        if (pos == std::string::npos)
        {
          std::cerr << "File " << *filename << " ignored" << std::endl;
          continue;
        }

        std::string extension = filename->substr(pos+1);

        std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);

        if ((extension == "jpg") || (extension == "jpeg"))
        {
          _jpegnames.push_back(*filename);
        }
        else if (extension == "gpx")
        {
          _gpxnames.push_back(*filename);
        }
        else
        {
          std::cerr << "Unsupported file " << *filename << " ignored" << std::endl;
        }
      }

      if (_jpegnames.empty())
      {
         std::cerr << "No photo files present" << std::endl;
         return false;
      }

      return true;
    }

    // -- Perform the geo-tagging -----------------------------------------------
    void perform()
    {
      for (auto photo = _jpegnames.begin(); photo != _jpegnames.end(); ++photo)
      {
        time_t time = readExiv2Time(*photo) + _offset;

        if (time == time_t(-1))
        {
          std::cerr << "No Exif.Image.DateTime present in " << *photo << std::endl;
          continue;
        }

        double lat;
        double lon;

        if (!findGeoPosition(time, lat,lon))
        {
          std::cerr << "No GeoPosition found for " << *photo << std::endl;
          continue;
        }

        if (!updateExiv2GeoPosition(*photo, lat, lon))
        {
          std::cerr << "Unable to update the GeoPosition for " << *photo << std::endl;
        }

        std::cout << std::fixed << std::setprecision(5) << "GeoPosition (" << lat << "," << lon << ") set for " << *photo << std::endl;
      }
    }

    // -- Parse a file ----------------------------------------------------------
    bool parseFile(const std::string &filename)
    {
      bool ok =false;

      std::ifstream file(filename);

      if (file.is_open())
      {
        ok = _xmlParser.parse(file);

        file.close();
      }
      else
      {
        std::cerr << "Unable to open: " << filename << std::endl;
      }

      return ok;
    }

  private:
    // -- Types -----------------------------------------------------------------
    struct Point
    {
      Point() : _lat(0.0), _lon(0.0), _time(-1) {}

      double  _lat;
      double  _lon;
      time_t  _time;
    };

    typedef std::vector<Point> Segment;

    typedef std::vector<Segment> Segments;


    // -- Methods ---------------------------------------------------------------
    static bool getDoubleAttribute(const Attributes &atts, const std::string &key, double &value)
    {
      auto iter = atts.find(key);

      if (iter == atts.end()) return false;

      if (iter->second.empty()) return false;

      try
      {
        value = std::stod(iter->second);

        return true;
      }
      catch(...)
      {
        return false;
      }
    }


    void processTimeStr(std::string &timeStr, time_t &time)
    {
      time = time_t(-1);

      XMLParser::trim(timeStr);

      if (timeStr.size() == 0) return;

      std::size_t index = timeStr.find(".");

      if (index != std::string::npos)
      {
        timeStr.erase(index, 4); // remove .000 millis
      }

      struct tm fields;

      memset(&fields, 0, sizeof(fields));

#if __GLIBC__
      if (strptime(timeStr.c_str(), "%FT%T%z", &fields) == nullptr)
#else
      if (strptime(timeStr.c_str(), "%Y-%m-%dT%TZ", &fields) == nullptr)
#endif
      {
        std::cout << "Failed to convert: " << timeStr << std::endl;
        return;
      }

      time = mktime(&fields) - ::timezone;
    }

    time_t readExiv2Time(const std::string &filename)
    {
      const std::string Exiv2key = "Exif.Image.DateTime";

      std::string command = "exiv2 -pt -g " + Exiv2key + " print " + filename;

      //std::cout << "Fetch: " << command << std::endl;

      FILE *file = nullptr;
      if ((file = popen(command.c_str(), "r")) == nullptr)
      {
        std::cerr << "Unable to start " << command << ", error: " << errno << std::endl;
        return  time_t(-1);
      }

      char buffer[512];
      while (fgets(buffer, sizeof(buffer), file) != nullptr)
      {
        std::string line(buffer);

        std::size_t index = 0;
        std::string key = readWord(line, index, ' ');
        readWord(line, index, ' '); // Type
        readWord(line, index, ' '); // Length
        std::string value = readWord(line, index, '\n');

        if ((key == Exiv2key) && (!value.empty()))
        {
          struct tm fields;

          memset(&fields, 0, sizeof(fields));

          if (strptime(value.c_str(), "%Y:%m:%d %T", &fields) != nullptr)
          {
            return mktime(&fields);
          }
        }
      }

      return time_t(-1);
    }

    std::string readWord(const std::string &line, std::size_t &index, char ch)
    {
      while ((index < line.size()) && (::isspace(line[index])))
      {
        index++;
      }

      std::string word;

      while ((index < line.size()) && (line[index] != ch))
      {
        word += line[index++];
      }

      return word;
    }

    bool findGeoPosition(time_t time, double &lat, double &lon)
    {
      for (auto segment = _segments.begin(); segment != _segments.end(); ++segment)
      {
        if (segment->empty()) continue;

        if ((time < segment->front()._time) || (time > segment->back()._time)) continue;

        auto last = segment->begin();
        for (auto point = segment->begin(); point != segment->end(); ++point)
        {
          if (time == point->_time)
          {
            lat = point->_lat;
            lon = point->_lon;
            return true;
          }

          if (time < point->_time)
          {
            lat = last->_lat + (point->_lat - last->_lat) * (double(time - last->_time) / double(point->_time - last->_time));
            lon = last->_lon + (point->_lon - last->_lon) * (double(time - last->_time) / double(point->_time - last->_time));
            return true;
          }

          last = point;
        }
      }

      return false;
    }

    bool updateExiv2GeoPosition(const std::string &photo, double lat, double lon)
    {
      std::string command = "exiv2 ";

      const std::string NS = (lat < 0 ? "S" : "N");

      command += "-M \"set Exif.GPSInfo.GPSLatitude " + deg2string(std::abs(lat)) + "\" ";
      command += "-M \"set Exif.GPSInfo.GPSLatitudeRef " + NS + "\" ";

      const std::string WE = (lon < 0 ? "W" : "E");

      command += "-M \"set Exif.GPSInfo.GPSLongitude " + deg2string(std::abs(lon)) + "\" ";
      command += "-M \"set Exif.GPSInfo.GPSLongitudeRef " + WE + "\" ";

      command += photo;

      // std::cout << "Update: " << command << std::endl;

      return (system(command.c_str()) == 0);
    }

    std::string deg2string(double position)
    {
      double degrees = 0.0;
      double minutes = 0.0;
      double seconds = 0.0;

      position = modf(position, &degrees);

      position *= 60.0;

      position = modf(position, &minutes);

      position *= (60.0 * 100.0);

      modf(position, &seconds);

      return std::to_string(int(degrees)) + "/1 " + std::to_string(int(minutes)) + "/1 " + std::to_string(int(seconds)) + "/100";
    }

  public:
    // -- Callbacks -------------------------------------------------------------
    virtual void startElement(const std::string &path, const std::string &name, const Attributes &attributes)
    {
      if (path == "/gpx/trk/trkseg")
      {
        _segments.push_back(Segment());
      }
      else if (path == "/gpx/trk/trkseg/trkpt")
      {
        Point point;

        if (getDoubleAttribute(attributes, "lat", point._lat) &&
            getDoubleAttribute(attributes, "lon", point._lon))
        {
          _segments.back().push_back(point);
        }
      }
      else if (path == "/gpx/trk/trkseg/trkpt/time")
      {
        // <time>2012-12-03T13:13:38Z</time>
        _timeStr.clear();
      }
    }

    virtual void text(const std::string &path, const std::string &text)
    {
      if (path == "/gpx/trk/trkseg/trkpt/time")
      {
        _timeStr.append(text);
      }
    }

    virtual void endElement(const std::string &path, const std::string &)
    {
      if (path == "/gpx/trk/trkseg/trkpt/time")
      {
        processTimeStr(_timeStr, _segments.back().back()._time);
      }
    }

  private:
    // -- Members ---------------------------------------------------------------
    arg::Arguments           _arguments;

    arg::Argument            _seconds;
    arg::Argument            _minutes;
    arg::Argument            _hours;

    XMLParser                _xmlParser;

    int                      _offset;

    std::vector<std::string> _jpegnames;
    std::vector<std::string> _gpxnames;

    Segments                 _segments;

    std::string              _timeStr;
  };
}

// -- Main program ------------------------------------------------------------
int main(int argc, char *argv[])
{
  gpxtools::GPXGeoTag gpxGeoTag;

  if (gpxGeoTag.processArguments(argc, argv))
  {
    gpxGeoTag.perform();

    return 0;
  }

  return 1;
}
